匹配几何效果(matchedGeometryEffect)
matchedGeometryEffect 用于在 不同视图之间建立几何关联关系,使视图在:
这些场景中,仍然保持 连续、平滑、空间一致的动画过渡效果。
该能力对应 SwiftUI 中的 matchedGeometryEffect,属于 组件级几何联动动画系统,不依赖导航系统。
一、API 定义
1matchedGeometryEffect?: {
2 id: string | number
3 namespace: NamespaceID
4 properties?: MatchedGeometryProperties
5 anchor?: Point | KeywordPoint
6 isSource?: boolean
7}
1type MatchedGeometryProperties = "frame" | "position" | "size"
二、核心作用
matchedGeometryEffect 的核心作用是:
让两个“逻辑上是同一个元素”的视图,在 不同布局结构中共享几何信息,从而产生连续的过渡动画。
它解决的问题包括:
- 视图从一个容器移动到另一个容器时的“跳变”
- 视图尺寸变化时的“突变”
- 列表项展开为详情页时的“断层感”
- Tab 切换指示器的“瞬移感”
三、参数详解
1. id(几何匹配唯一标识)
-
用于标识这是 哪一个几何元素
-
在同一个 namespace 下:
-
通常来自:
规则:
-
id 必须稳定
-
动画期间不能频繁变化
-
同一时刻:
- 一个 id 只能有一个
isSource = true
2. namespace(几何命名空间)
规则:
- source 与 target 必须使用 同一个 namespace
- 不允许跨 namespace 匹配
3. properties(参与匹配的几何属性)
1properties?: "frame" | "position" | "size"
默认值:
含义说明:
| 值 |
含义 |
"frame" |
同时匹配位置 + 尺寸 |
"position" |
仅匹配中心点位置 |
"size" |
仅匹配尺寸,不匹配位置 |
选择原则:
"frame":最完整、最自然的动画
"position":指示器、滑块、选中背景
"size":放大缩小、展开收起
4. anchor(锚点)
1anchor?: Point | KeywordPoint
默认值:
作用:
常见取值:
"center"
"topLeading"
"topTrailing"
"bottomLeading"
"bottomTrailing"
使用场景:
- 卡片从左上角展开
- 头像从右上角放大
- 底部元素向上弹出
5. isSource(是否作为几何数据的“源”)
默认值:
含义说明:
| 值 |
行为 |
true |
当前视图向外“提供”几何数据 |
false |
当前视图“接收”几何动画结果 |
标准使用模式:
- 原始视图:
isSource = true
- 目标视图:
isSource = false
如果省略:
- 第一个出现的视图默认作为 source
- 其余作为接收方
四、最小可用示例(位置 + 尺寸联动)
该示例演示:
一个圆形在两个区域之间切换位置与尺寸,并保持连续动画。
1const expanded = useObservable(false)
2
3return <NamespaceReader>
4 {namespace => (
5 <VStack spacing={40}>
6 <Button
7 title="Toggle"
8 action={() => {
9 expanded.setValue( !expanded.value)
10 }}
11 />
12
13 <ZStack
14 frame={{ width: 300, height: 200 }}
15 background="systemGray6"
16 >
17 {!expanded.value && (
18 <Circle
19 fill="systemOrange"
20 frame={{ width: 60, height: 60 }}
21 matchedGeometryEffect={{
22 id: "circle",
23 namespace
24 }}
25 />
26 )}
27 </ZStack>
28
29 <ZStack
30 frame={{ width: 300, height: 300 }}
31 background="systemGray4"
32 >
33 {expanded.value && (
34 <Circle
35 fill="systemOrange"
36 frame={{ width: 150, height: 150 }}
37 matchedGeometryEffect={{
38 id: "circle",
39 namespace,
40 isSource: false
41 }}
42 />
43 )}
44 </ZStack>
45 </VStack>
46 )}
47</NamespaceReader>
该示例实现的动画效果:
五、仅同步“位置”的示例(指示器动画)
1const selected = useObservable(0)
2
3return <NamespaceReader>
4 {namespace => (
5 <HStack spacing={24}>
6 <Text
7 onTapGesture={() => selected.setValue(0)}
8 matchedGeometryEffect={{
9 id: "indicator",
10 namespace,
11 properties: "position",
12 isSource: selected.value === 0
13 }}
14 >
15 Tab 1
16 </Text>
17
18 <Text
19 onTapGesture={() => selected.setValue(1)}
20 matchedGeometryEffect={{
21 id: "indicator",
22 namespace,
23 properties: "position",
24 isSource: selected.value === 1
25 }}
26 >
27 Tab 2
28 </Text>
29 </HStack>
30 )}
31</NamespaceReader>
适用于:
六、仅同步“尺寸”的示例(放大缩小)
1const expanded = useObservable(false)
2
3return <NamespaceReader>
4 {namespace => (
5 <ZStack>
6 <Circle
7 fill="systemBlue"
8 frame={{
9 width: expanded.value ? 200 : 80,
10 height: expanded.value ? 200 : 80
11 }}
12 matchedGeometryEffect={{
13 id: "avatar",
14 namespace,
15 properties: "size"
16 }}
17 onTapGesture={() => {
18 expanded.setValue(!expanded.value)
19 }}
20 />
21 </ZStack>
22 )}
23</NamespaceReader>
适用于:
七、多元素联动示例(卡片 → 详情)
1<NamespaceReader>
2 {namespace => (
3 <ZStack>
4 {!showDetail.value && (
5 <VStack spacing={16}>
6 <Image
7 source="cover"
8 matchedGeometryEffect={{
9 id: "card.image",
10 namespace
11 }}
12 />
13 <Text
14 matchedGeometryEffect={{
15 id: "card.title",
16 namespace
17 }}
18 >
19 Card Title
20 </Text>
21 </VStack>
22 )}
23
24 {showDetail.value && (
25 <VStack spacing={24}>
26 <Image
27 source="cover"
28 frame={{ width: 300, height: 200 }}
29 matchedGeometryEffect={{
30 id: "card.image",
31 namespace,
32 isSource: false
33 }}
34 />
35 <Text
36 font="largeTitle"
37 matchedGeometryEffect={{
38 id: "card.title",
39 namespace,
40 isSource: false
41 }}
42 >
43 Card Title
44 </Text>
45 </VStack>
46 )}
47 </ZStack>
48 )}
49</NamespaceReader>
效果说明:
- 图片与标题同时参与几何匹配
- 从卡片形态平滑过渡为详情页布局
- 无需使用导航动画
八、关键使用规则总结
-
namespace 必须完全相同
-
id 必须完全一致
-
同一时刻:
- 一个 id 只能有一个
isSource = true
-
默认行为:
1properties = "frame"
2anchor = "center"
3isSource = true
-
source 与 target 必须:
-
如果 source 和 target:
- 同时存在,且都为
isSource = true
→ 动画不确定,可能失效
-
Widget 与 Live Activity 环境不支持完整 matchedGeometry 动画能力
九、适用场景总结
适合使用 matchedGeometryEffect 的场景:
- Tab 指示器动画
- 卡片 → 详情展开
- 图片放大预览
- 列表项选中动画
- 分栏布局中的选中项切换
不适合使用的场景:
- 高频数据刷新列表
- 大量同时进行几何动画的复杂视图树
- 帧率敏感的实时图表